package com.gmrz.uaf.protocol.v1.schema;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.google.common.io.LittleEndianDataOutputStream;
import com.gmrz.uaf.common.Constants;
import com.gmrz.uaf.protocol.v1.processor.exception.TLVParsingException;
import com.gmrz.uaf.protocol.v1.processor.exception.UAFErrorCode;
import com.gmrz.util.Convert;

public class SignData {
    private static final Logger LOG = LogManager.getLogger(SignData.class);

    private transient short authenticatorVersion = 0;
    private transient byte authenticationMode = 0;
    private transient int signAlgorithm = 0;
    private transient String aaid = null;
    private transient byte[] keyID = null;
    private transient byte[] authenticatorNonce = null;
    private transient byte[] hashedFinalChallenge = null;
    private transient byte[] transactionTextHash = null;
    private transient byte[] uvi = null;
    private transient int signCounter = 0;
    private transient List<byte[]> uvs = null;
    private transient byte[] certAuthTradeIshow = null;
    private transient byte[] certAuthTradeShowdataHash = null;
    private transient String deviceID = null;

    private transient byte[] tagCertIsDeviceKeySign = null;

    private byte[] receivedBytes = null;

    public byte[] getCertAuthTradeIshow() {
        return certAuthTradeIshow;
    }

    public void setCertAuthTradeIshow(byte[] certAuthTradeIshow) {
        this.certAuthTradeIshow = certAuthTradeIshow;
    }

    public byte[] getCertAuthTradeShowdataHash() {
        return certAuthTradeShowdataHash;
    }

    public void setCertAuthTradeShowdataHash(byte[] certAuthTradeShowdataHash) {
        this.certAuthTradeShowdataHash = certAuthTradeShowdataHash;
    }

    public List<byte[]> getUvs() {
        return uvs;
    }

    public void setUvs(List<byte[]> uvs) {
        this.uvs = uvs;
    }

    public void setReceivedBytes(byte[] krdBytes) {
        this.receivedBytes = krdBytes;
    }

    public byte[] getReceivedBytes() {
        return this.receivedBytes;
    }

    public short getAuthenticatorVersion() {
        return this.authenticatorVersion;
    }

    public SignData withAuthenticatorVersion(short authenticatorVersion) {
        this.authenticatorVersion = authenticatorVersion;
        return this;
    }

    public byte getAuthenticationMode() {
        return this.authenticationMode;
    }

    public SignData withAuthenticationMode(byte authenticationMode) {
        this.authenticationMode = authenticationMode;
        return this;
    }

    public int getSignAlgorithm() {
        return this.signAlgorithm;
    }

    public SignData withSignAlgorithm(int signAlgorithm) {
        this.signAlgorithm = signAlgorithm;
        return this;
    }

    public String getAAID() {
        return this.aaid;
    }

    public SignData withAAID(String aaid) {
        this.aaid = aaid;
        return this;
    }

    public byte[] getKeyID() {
        return this.keyID;
    }

    public SignData withKeyID(byte[] keyID) {
        this.keyID = keyID;
        return this;
    }

    public byte[] getAuthenticatorNonce() {
        return this.authenticatorNonce;
    }

    public SignData withAuthenticatorNonce(byte[] authenticatorNonce) {
        this.authenticatorNonce = authenticatorNonce;
        return this;
    }

    public byte[] getHashedFinalChallenge() {
        return this.hashedFinalChallenge;
    }

    public SignData withHashedFinalChallenge(byte[] hashedFinalChallenge) {
        this.hashedFinalChallenge = hashedFinalChallenge;
        return this;
    }

    public byte[] getTransactionTextHash() {
        return this.transactionTextHash;
    }

    public SignData withTransactionTextHash(byte[] transactionTextHash) {
        this.transactionTextHash = transactionTextHash;
        return this;
    }

    public int getSignCounter() {
        return this.signCounter;
    }

    public SignData withSignCounter(int signCounter) {
        this.signCounter = signCounter;
        return this;
    }

    public byte[] getUvi() {
        return this.uvi;
    }

    public void setUvi(byte[] uvi) {
        this.uvi = uvi;
    }

    public SignData withUvi(byte[] uvi) {
        this.uvi = uvi;
        return this;
    }

    public String getDeviceID() {
        return deviceID;
    }

    public void setDeviceID(String deviceID) {
        this.deviceID = deviceID;
    }

    public byte[] getTagCertIsDeviceKeySign() {
        return tagCertIsDeviceKeySign;
    }

    public void setTagCertIsDeviceKeySign(byte[] tagCertIsDeviceKeySign) {
        this.tagCertIsDeviceKeySign = tagCertIsDeviceKeySign;
    }

    public byte[] toTLVBytes() throws TLVParsingException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        LittleEndianDataOutputStream ledo = new LittleEndianDataOutputStream(
                baos);
        try {
            byte[] aaidBytes = this.aaid == null ? new byte[0] : Convert
                    .toUtf8(this.aaid);
            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_AAID.getTagValue());
            ledo.writeShort(aaidBytes.length);
            ledo.write(aaidBytes);

            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_ASSERTION_INFO
                    .getTagValue());
            ledo.writeShort(5);
            ledo.writeShort(this.authenticatorVersion);
            ledo.writeByte(this.authenticationMode);
            ledo.writeShort(this.signAlgorithm);

            byte[] authenticatorNonceBytes = this.authenticatorNonce == null ? new byte[0]
                    : this.authenticatorNonce;
            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_AUTHENTICATOR_NONCE
                    .getTagValue());
            ledo.writeShort(authenticatorNonceBytes.length);
            ledo.write(authenticatorNonceBytes);

            byte[] hashedFinalChallengeBytes = this.hashedFinalChallenge == null ? new byte[0]
                    : this.hashedFinalChallenge;
            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_FINAL_CHALLENGE
                    .getTagValue());
            ledo.writeShort(hashedFinalChallengeBytes.length);
            ledo.write(hashedFinalChallengeBytes);

            byte[] transactionTextHashBytes = this.transactionTextHash == null ? new byte[0]
                    : this.transactionTextHash;
            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_TRANSACTION_CONTENT_HASH
                    .getTagValue());
            ledo.writeShort(transactionTextHashBytes.length);
            ledo.write(transactionTextHashBytes);

            byte[] keyIDBytes = this.keyID == null ? new byte[0] : this.keyID;
            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_KEYID.getTagValue());
            ledo.writeShort(keyIDBytes.length);
            ledo.write(keyIDBytes);

            if (this.uvi != null) {
                ledo.writeShort(Constants.UAFV1AssertionTag.AFI_USER_VERIFICATION_INDEX
                        .getTagValue());
                ledo.writeShort(this.uvi.length);
                ledo.write(this.uvi);
            }

            ledo.writeShort(Constants.UAFV1AssertionTag.TAG_COUNTERS
                    .getTagValue());
            ledo.writeShort(4);
            ledo.writeInt(this.signCounter);

            ledo.close();
        } catch (IOException ioe) {
            LOG.error("Failed to create TLV for Authentication SignData", ioe);
            throw new TLVParsingException(
                    UAFErrorCode.PROTOCOL_TLV_PARSING_FAILED, ioe);
        }

        return baos.toByteArray();
    }

    public SignData fromTLVBytes(byte[] data) throws TLVParsingException {
        ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        try {
            while (buf.hasRemaining()) {
                short tag = buf.getShort();
                Constants.UAFV1AssertionTag t = Constants.UAFV1AssertionTag
                        .getTagForValue(tag);
                short len;
                byte[] val;
                switch (t.getTagValue()) {
                    case 11787:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        this.aaid = Convert.fromUtf8(val);
                        break;
                    case 11790:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        ByteBuffer subBuf = ByteBuffer.wrap(val).order(
                                ByteOrder.LITTLE_ENDIAN);
                        if (len >= 5) {
                            this.authenticatorVersion = subBuf.getShort();
                            this.authenticationMode = subBuf.get();
                            this.signAlgorithm = subBuf.getShort();
                        } else {
                            this.authenticatorVersion = subBuf.getShort();
                            this.authenticationMode = subBuf.get();
                        }
                        break;
                    case 11791:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        this.authenticatorNonce = val;
                        break;
                    case 11786:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        this.hashedFinalChallenge = val;
                        break;
                    case 11792:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        this.transactionTextHash = val;
                        break;
                    case 11785:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        this.keyID = val;
                        break;
                    case 11789:
                        len = buf.getShort();
                        if (len >= 4)
                            this.signCounter = buf.getInt();
                        break;
                    case 259:
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                        this.uvi = val;
                        break;
                    case 260:
                        if (this.uvi == null) {
                            len = buf.getShort();
                            val = new byte[len];
                            buf.get(val);
                            this.uvi = val;
                        }
                        break;
                    default:
                        LOG.debug("Unrecognized tag in Authentication Sign Data[ "
                                + tag + "]");
                        len = buf.getShort();
                        val = new byte[len];
                        buf.get(val);
                }
            }
        } catch (Exception ex) {
            LOG.error("Error parsing Authenticator Sign TLV data, ", ex);
            throw new TLVParsingException(
                    UAFErrorCode.PROTOCOL_INVALID_AUTH_ASSERTION, ex);
        }
        return this;
    }
}
